/**************************************
 * MultiPartForm.java
 * Provides MPF support for browser 
 * challenged environs.  
 * 
 * Notes =
 * - Does not depend on Android!
 *   Use anytime / anywhere to
 *   talk MPF to web servers.
 *   
 *  - Recommend using O'Reilly's 
 *    MultiPartParser on server
 *    side.  We use it in JBoss 
 *    for catching throws from
 *    android using this class.  
 *    (google: com.ora Multipart)
 *     
 */
package org.puresilk.android.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.spec.EncodedKeySpec;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Vector;

import org.puresilk.android.net.HttpResponseDelegate;

/**
 * Supports sending out simple MPF 
 * mime formats for uploading file
 * data to a web server app via 
 * standard CGI / HTTP transmission.
 * 
 * @author david
 */
public class MultiPartForm {
	// private static final Logger logger = Logger.getInstance("MutliPartForm");
	private static boolean DEBUG = false;
	
	// rendering formats
	//private static final String MPF_HEADER = "Content-Type: multipart/form-data; boundary=%s\nContent-Length: %%d\n";
	private static final String CD_FIELD_FMT = "--%s\nContent-Disposition: form-data; name=\"%s\"\n\n%s\n\n";
	private static final String CD_FILE_FMT = "--%s\nContent-Disposition: form-data; name=\"upload_file_%d\"; filename=\"%s\"\nContent-Type: %s\n\n";
	private static final String FILE_MARKER_FMT = "%s%d%s%s%s\n";
	private static final String FILE_MARKER_DLMTR = "#-#";
	private static final String FILE_NAME_DLMTR = "~F~";
	private static final String DEFAULT_UPLOAD_TYPE = "application/octet-stream";
	
	private static final int READ_SIZE = 1024 * 1024;
	
	// internal data structures
	private Map<String,String> dataFlds = new HashMap<String,String>();
	Vector<UploadFile> uploadFiles = new Vector<UploadFile>();
	private int totalFileSize = 0;

	// internal buffer used for Render...
	private byte [] _mdata = null;
	
	// boundary issues
	private String boundary = "---------------------------" + randomString() + randomString() + randomString();
	private static Random random = new Random();
	
	/**
	 * Utility function for generating boundary
	 * signatures.
	 */
	protected static String randomString() {
		return Long.toString(random.nextLong(), 36);
	}
	
	/**
	 * convenience data container for
	 * upload file info.
	 */
	class UploadFile{
		File file = null;
		String mimeType = null;
		
		/** constructor */
		public UploadFile(File f, String mType){
			file = f;
			mimeType = mType;
		}

		// Setters / Getters
		public String getName(){
			String filename;
			try {
				filename = URLEncoder.encode(file.getName(),"UTF-8");
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				filename = file.getName();
			}
			return filename; 
		}
		
		public long getLength(){
			return file.length();
		}
		
		public File getFile() {
			return file;
		}

		public void setFile(File file) {
			this.file = file;
		}

		public String getMimeType() {
			return mimeType == null ? DEFAULT_UPLOAD_TYPE : mimeType ;
		}

		public void setMimeType(String mimeType) {
			this.mimeType = mimeType;
		}		
	}
	
	//-----------------------
	// MPF API
	//-----------------------
	

	/**
	 * Public Constructor
	 */
	public MultiPartForm() { }

	/**
	 * Return the unique boundary divider for
	 * this MPF.
	 */
	public String boundary() {
	    return boundary;
	}
	
	/**
	 * Add a name / value pair to the form 
	 */
	public void addField(String name, String data){
		dataFlds.put(name, data);
	}

	/**
	 * Add file with default mime type 
	 * (application/octet-stream)
	 * 
	 * @param fName
	 */
	public void addFile(String fName) throws IOException {
		addFile(fName,null);
	}
	
	/**
	 * Add file to request w/ mime type
	 * @param fName
	 * @param mimeType
	 */
	public void addFile(String fName, String mimeType) throws IOException {
		// save file info
		byte [] buff = new byte [READ_SIZE];
		int bCount = 0;
		int bRead = 0;
		UploadFile uf = new UploadFile(new File(fName),mimeType);
		uploadFiles.add(uf);
		FileInputStream of = new FileInputStream(uf.getFile());
		while(bRead != -1){
			bCount += bRead;
			bRead = of.read(buff,0, READ_SIZE);
		}
		
		// including size
		totalFileSize += uf.getLength();	
	}

	/**
	 * Return value for Content Type header (HTTP)
	 * @return
	 */
	public String getContentType() {
		return "multipart/form-data; boundary="+boundary;
	}
	
	/**
	 * Return value for Content Length header (HTTP)
	 * @return
	 */	
	public int getContentSize() {
		// start with total file size
		int bsize = totalFileSize;
		
		// add up text chunk sizes (skipping file nodes)
		String [] mpf_parts = dump().split(FILE_MARKER_DLMTR);
		for(int cnt=0;cnt < mpf_parts.length;cnt++){
			String mp = mpf_parts[cnt];
			if(mp.contains(FILE_NAME_DLMTR))
				continue;
			bsize += mpf_parts[cnt].getBytes().length;
		}
		// send back full size
		return bsize;
	}
	
	/**
	 * Fetch bytes 
	 * @return
	 */
	public byte [] getBytes() throws Exception {
		return _mdata == null ? render() : _mdata;
	}
	
	/**
	 * produce a byte array that represents the
	 * data in this MPF.  
	 * 
	 * (Usable but takes memory, see write() and dump())
	 * 
	 * @return
	 * @throws Exception
	 */
	public byte[] render() throws Exception {
		// parse out the mpf
		String [] mpf_parts = dump().split(FILE_MARKER_DLMTR);

		_mdata = new byte[getContentSize()];
		int ofs = 0;

		// iterate through the parts
		for(int part=0; part < mpf_parts.length; part++){
			// check for node type
			String [] subs = mpf_parts[part].split(FILE_NAME_DLMTR);
			if (subs.length > 1) {
				// file node
				UploadFile uf = uploadFiles.get(Integer.parseInt(subs[0]));
				if( uf.getName().equals(subs[1]) )
					// read straight from file
					ofs = getDataFromFile(_mdata,ofs,uf.getFile());
				else
					// nah!
					System.err.println(String.format("!! File Name Mismatch %s / %s", uf.getName(),subs[1]));	
			}
			else {
				// text nome
				byte [] d1 = mpf_parts[part].getBytes();
				for(int p=0;p<d1.length;p++){
					_mdata[ofs] = d1[p];
					ofs++;
				}
			}		
		}
		
		// ok, send it all back
		return _mdata;
	}
	
	/**
	 * Write form directly to output stream.
	 * (Much more efficient than render)
	 * 
	 * @param os
	 */
	public void write(OutputStream os, HttpResponseDelegate dlgt) throws IOException {
		// get the text and parse on file slots
		String [] mpf_parts = dump().split(FILE_MARKER_DLMTR);
		
		for(int part=0; part < mpf_parts.length; part++){
			String [] subs = mpf_parts[part].split(FILE_NAME_DLMTR);
			
			if (subs.length > 1) {
				// file node
				UploadFile uf = uploadFiles.get(Integer.parseInt(subs[0]));
				if( uf.getName().equals(subs[1]) ){
					int x = pipe(os,uf.getFile(),dlgt);
					
					System.out.println(String.format("~!~ File %s bytes written %d", uf.getName(),x));	
				}
				else
					// nah!
					System.err.println(String.format("!! File Name Mismatch %s / %s", uf.getName(),subs[1]));	
			}
			else{
				// text node
				os.write(mpf_parts[part].getBytes());
			}
		}				
	}
		
	/**
	 * Create text form with place holders for file data
	 * 
	 * @return
	 */
	public String dump() {
		StringBuffer work = new StringBuffer();
		
		// format all of the data fields
		Iterator<String> itr = dataFlds.keySet().iterator();
		while(itr.hasNext()){
			String n = itr.next();
			String v = dataFlds.get(n);
		
			work.append(String.format(CD_FIELD_FMT, boundary,n,v));
		}
		
		// format all of the file data
		for(int ndx=0;ndx<uploadFiles.size();ndx++) {
			UploadFile uf = uploadFiles.get(ndx);
				// build section 
				work.append(String.format(CD_FILE_FMT,boundary,ndx,uf.getName(),uf.getMimeType()));		
				work.append(String.format(FILE_MARKER_FMT, FILE_MARKER_DLMTR, ndx, FILE_NAME_DLMTR, uf.getName(),FILE_MARKER_DLMTR));
		}		

		// end the form data
		work.append(String.format("--%s--\n\n",boundary));
		
		if(DEBUG)
			System.out.println("MPF ==>\n" + work.toString());
		
		// send back results 
		return work.toString();
	}
	
	/**
	 * fill byte array from offset from 
	 * file and return new offset.
	 * 
	 * @param data
	 * @param offset
	 * @param f
	 * @return
	 * @throws IOException
	 */
	protected int getDataFromFile(byte [] data, int offset, File f)  throws IOException {
		int bRead = 0;
		byte [] buff = new byte [READ_SIZE];
		
		FileInputStream fis = new FileInputStream(f);
		try {
			// read into buffer
			while (bRead != -1) {
				// get bytes
				bRead = fis.read(buff,0,(int)(f.length() > READ_SIZE ? READ_SIZE : f.length()));

				if(bRead > 0)
					// transfer
					for(int x=0;x<bRead;x++) {
						data[offset] = buff[x];
						offset++;
					}
			}
			fis.close();
		}
		catch (IOException ioe) {
			// uh-oh
			try {fis.close();} catch(Exception ex) {}
			throw ioe;
		}
		// return next write position
		return offset;
	}
	
	/**
	 * pipe data from file
	 * to output stream via 
	 * 8k buffer.
	 * @param dlgt 
	 */
	protected int pipe(OutputStream os, File f, HttpResponseDelegate dlgt)  throws IOException {
		int bRead = 0;
		// overides 1mb buffer with 8k buffer for dialog reporting. delete to have default.
		int READ_SIZE = 8192;
		
		byte [] buffer = new byte [READ_SIZE];
		int tot = 0;
		
		FileInputStream fis = new FileInputStream(f);
		try {
			// read into buffer
			while (bRead != -1) {
				// bump that one
				tot += bRead;
				// read
				bRead = fis.read(buffer, 0, READ_SIZE);
				
				if (bRead > 0) {
					// and write
					dlgt.onDataWrite(tot);
					os.write(buffer,0,bRead);
				}
			}
		}
		catch (IOException ioe) {
			// uh-oh
			System.err.println("!! IO Error Piping file data");
			ioe.printStackTrace();
			
			try {fis.close();} catch(Exception ex) {}
			buffer = null;
			throw ioe;
		}
		
		// close it up
		buffer = null;
		fis.close();
		
		// size of read
		return tot;
	}
}
